《Flutter 实战》笔记

观书有会意处,题其衣裳,以记其事~

总体介绍

1.1 Flutter框架结构

无论学什么技术,都要现有一张清晰的“地图”,而我们的学习过程就是“按图索骥”,这样我们才不会陷于细节而“目无全牛”。言归正传,我们看一下Flutter官方提供的Flutter框架图,如图1-1所示:

图1-1

Flutter Framework

这是一个纯 Dart实现的 SDK,它实现了一套基础库,自底向上,我们来简单介绍一下:

  • 底下两层(Foundation和Animation、Painting、Gestures)在Google的一些视频中被合并为一个dart UI层,对应的是Flutter中的dart:ui包,它是Flutter引擎暴露的底层UI库,提供动画、手势及绘制能力。
  • Rendering层,这一层是一个抽象的布局层,它依赖于dart UI层,Rendering层会构建一个UI树,当UI树有变化时,会计算出有变化的部分,然后更新UI树,最终将UI树绘制到屏幕上,这个过程类似于React中的虚拟DOM。Rendering层可以说是Flutter UI框架最核心的部分,它除了确定每个UI元素的位置、大小之外还要进行坐标变换、绘制(调用底层dart:ui)。
  • Widgets层是Flutter提供的的一套基础组件库,在基础组件库之上,Flutter还提供了 Material 和Cupertino两种视觉风格的组件库。而我们Flutter开发的大多数场景,只是和这两层打交道

Flutter Engine

这是一个纯 C++实现的 SDK,其中包括了 Skia引擎、Dart运行时、文字排版引擎等。在代码调用 dart:ui库时,调用最终会走到Engine层,然后实现真正的绘制逻辑。

1.2 AOT & JIT

程序主要有两种运行方式:静态编译与动态解释。静态编译的程序在执行前全部被翻译为机器码,通常将这种类型称为AOT (Ahead of time)即 “提前编译”;而解释执行的则是一句一句边翻译边运行,通常将这种类型称为JIT(Just-in-time)即“即时编译”。AOT程序的典型代表是用C/C++开发的应用,它们必须在执行前编译成机器码,而JIT的代表则非常多,如JavaScript、python等,事实上,所有脚本语言都支持JIT模式。但需要注意的是JIT和AOT指的是程序运行方式,和编程语言并非强关联的,有些语言既可以以JIT方式运行也可以以AOT方式运行,如Java、Python,它们可以在第一次执行时编译成中间字节码、然后在之后执行时可以直接执行字节码,也许有人会说,中间字节码并非机器码,在程序执行时仍然需要动态将字节码转为机器码,是的,这没有错,不过通常我们区分是否为AOT的标准就是看代码在执行之前是否需要编译,只要需要编译,无论其编译产物是字节码还是机器码,都属于AOT。在此,读者不必纠结于概念,概念就是为了传达精神而发明的,只要读者能够理解其原理即可,得其神忘其形。

Dart

1.1 内建类型

Dart 语言支持以下内建类型:

  • Number(包含int、double)
  • String
  • Boolean
  • List (也被称为 Array)
  • Map
  • Set
  • Rune (用于在字符串中表示 Unicode 字符)
  • Symbol

这些类型都可以被初始化为字面量。 例如, 'this is a string' 是一个字符串的字面量, true 是一个布尔的字面量。

因为在 Dart 所有的变量终究是一个对象(一个类的实例), 所以变量可以使用 构造涵数 进行初始化。 一些内建类型拥有自己的构造函数。 例如, 通过 Map() 来构造一个 map 变量。

1.2 函数

1.2.1 可选参数

  • 命名可选参数
  • 位置可选参数
  • 默认参数

1.2.2 函数是一等对象

一个函数可以作为另一个函数的参数。 例如:

1
2
3
4
5
6
7
8
void printElement(int element) {
print(element);
}

var list = [1, 2, 3];

// 将 printElement 函数作为参数传递。
list.forEach(printElement);

同样可以将一个函数赋值给一个变量,例如:

1
2
var loudify = (msg) => '!!! ${msg.toUpperCase()} !!!';
assert(loudify('hello') == '!!! HELLO !!!');

1.2.3 匿名函数

1
([[*Type*] *param1*[, …]]) { *codeBlock*;};

下面例子中定义了一个包含一个无类型参数 item 的匿名函数。 list 中的每个元素都会调用这个函数,打印元素位置和值的字符串。

1
2
3
4
var list = ['apples', 'bananas', 'oranges'];
list.forEach((item) {
print('${list.indexOf(item)}: $item');
});

如果函数只有一条语句, 可以使用箭头简写。

1
2
list.forEach(
(item) => print('${list.indexOf(item)}: $item'));

1.2.4 返回值

所有函数都会返回一个值。 如果没有明确指定返回值, 函数体会被隐式的添加 return null; 语句。

1.3、运算符

1.3.1 赋值运算符

使用 = 为变量赋值。 使用 ??= 运算符时,只有当被赋值的变量为 null 时才会赋值给它。

1
2
3
4
// 将值赋值给变量a
a = value;
// 如果b为空时,将变量赋值给b,否则,b的值保持不变。
b ??= value;

复合赋值运算符:

Compound assignment Equivalent expression
For an operator *op*: a *op*= b a = a *op* b
Example: a += b a = a + b

1.3.2 条件表达式

Dart有两个运算符,有时可以替换 if-else 表达式, 让表达式更简洁:

  • *condition* ? *expr1* : *expr2*

    如果条件为 true, 执行 expr1 (并返回它的值): 否则, 执行并返回 expr2 的值。

  • *expr1* ?? *expr2*

    如果 expr1 是 non-null, 返回 expr1 的值; 否则, 执行并返回 expr2 的值。

如果赋值是根据布尔值, 考虑使用 ?:

1
var visibility = isPublic ? 'public' : 'private';

如果赋值是基于判定是否为 null, 考虑使用 ??

1
String playerName(String name) => name ?? 'Guest';

1.3.3 级联运算符 (..)

级联运算符 (..) 可以实现对同一个对像进行一系列的操作。 除了调用函数, 还可以访问同一对象上的字段属性。 这通常可以节省创建临时变量的步骤, 同时编写出更流畅的代码。

考虑一下代码:

1
2
3
4
querySelector('#confirm') // 获取对象。
..text = 'Confirm' // 调用成员变量。
..classes.add('important')
..onClick.listen((e) => window.alert('Confirmed!'));

第一句调用函数 querySelector() , 返回获取到的对象。 获取的对象依次执行级联运算符后面的代码, 代码执行后的返回值会被忽略。

上面的代码等价于:

1
2
3
4
var button = querySelector('#confirm');
button.text = 'Confirm';
button.classes.add('important');
button.onClick.listen((e) => window.alert('Confirmed!'));

级联运算符可以嵌套,例如:

1
2
3
4
5
6
7
8
final addressBook = (AddressBookBuilder()
..name = 'jenny'
..email = 'jenny@example.com'
..phone = (PhoneNumberBuilder()
..number = '415-555-0100'
..label = 'home')
.build())
.build();

在返回对象的函数中谨慎使用级联操作符。 例如,下面的代码是错误的:

1
2
3
var sb = StringBuffer();
sb.write('foo')
..write('bar'); // Error: 'void' 没哟定义 'write' 函数。

sb.write() 函数调用返回 void, 不能在 void 对象上创建级联操作。

1.4 控制流程语句

1.4.1 for 循环

如果要迭代一个实现了 Iterable 接口的对象, 可以使用 forEach() 方法, 如果不需要使用当前计数值, 使用 forEach() 是非常棒的选择;

1
candidates.forEach((candidate) => candidate.interview());

实现了 Iterable 的类(比如, List 和 Set)同样也支持使用 for-in 进行迭代操作 iteration

1
2
3
4
var collection = [0, 1, 2];
for (var x in collection) {
print(x); // 0 1 2
}

1.5 类

1.5.1 获取对象的类型

使用对象的 runtimeType 属性, 可以在运行时获取对象的类型, runtimeType 属性回返回一个 Type 对象。

1
print('The type of a is ${a.runtimeType}');

1.5.2 构造函数

1.5.2.1 构造函数语法糖
1
2
3
4
5
6
7
8
9
class Point {
num x, y;

Point(num x, num y) {
// 还有更好的方式来实现下面代码,敬请关注。
this.x = x;
this.y = y;
}
}
1
2
3
4
5
6
7
class Point {
num x, y;

// 在构造函数体执行前,
// 语法糖已经设置了变量 x 和 y。
Point(this.x, this.y);
}
1.5.2.2 命名构造函数

使用命名构造函数可为一个类实现多个构造函数, 也可以使用命名构造函数来更清晰的表明函数意图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Point {
num x, y;

Point(this.x, this.y);

Point.rect(this.x, this.y);

void pointPrint() {
print('x = $x , y = $y ');
}
}

main() {
Point r = Point(2, 2);
r.pointPrint();

Point q = Point.rect(5, 5);
q.pointPrint();
}
1.5.2.3 重定向构造函数
1
2
3
4
5
class Rect{
num x ,y , wid , hei ;
Rect(this.x , this.y ,this.wid ,this.hei);
Rect.withSize(num width,num height) : this(0 , 0 , width ,height) ;
}
1.5.2.4 常量构造函数

如果该类生成的对象是固定不变的, 那么就可以把这些对象定义为编译时常量。 为此,需要定义一个 const 构造函数, 并且声明所有实例变量为 final

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ImmutablePoint {
final num x, y;
const ImmutablePoint(this.x, this.y);
static final ImmutablePoint origin = const ImmutablePoint(0, 0);
}

void main() {
var a = const ImmutablePoint(1, 1);
var b = ImmutablePoint(1, 1);
var c = const ImmutablePoint(0, 0);

print("a hash code: " + a.hashCode.toString());
print("b hash code: " + b.hashCode.toString());
print("c hash code: " + c.hashCode.toString());
print("origin hash code:" + ImmutablePoint.origin.hashCode.toString());
}

// 输出结果
a hash code: 888999976
b hash code: 635374739
c hash code: 311897067
origin hash code:311897067
1.5.2.5 工厂构造函数

当执行构造函数并不总是创建这个类的一个新实例时,则使用 factory 关键字。 例如,一个工厂构造函数可能会返回一个 cache 中的实例, 或者可能返回一个子类的实例。

以下示例演示了从缓存中返回对象的工厂构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Logger {
final String name;
bool mute = false;

// 从命名的 _ 可以知,
// _cache 是私有属性。
static final Map<String, Logger> _cache =
<String, Logger>{};

factory Logger(String name) {
if (_cache.containsKey(name)) {
return _cache[name];
} else {
final logger = Logger._internal(name);
_cache[name] = logger;
return logger;
}
}

Logger._internal(this.name);

void log(String msg) {
if (!mute) print(msg);
}
}
1.5.2.6 noSuchMethod()

当代码尝试使用不存在的方法或实例变量时, 通过重写 noSuchMethod() 方法,来实现检测和应对处理:

1
2
3
4
5
6
7
8
9
class A {
// 如果不重写 noSuchMethod,访问
// 不存在的实例变量时会导致 NoSuchMethodError 错误。
@override
void noSuchMethod(Invocation invocation) {
print('You tried to use a non-existent member: ' +
'${invocation.memberName}');
}
}

1.6 泛型

1.6.1 为什么使用泛型

1.6.1.1 正确指定泛型类型可以提高代码质量

如果想让 List 仅仅支持字符串类型, 可以将其声明为 List (读作“字符串类型的 list ”)。 那么,当一个非字符串被赋值给了这个 list 时,开发工具就能够检测到这样的做法可能存在错误。 例如:

1
2
3
var names = List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
names.add(42); // 错误
1.6.1.2 使用泛型可以减少重复的代码
1
2
3
4
abstract class ObjectCache {
Object getByKey(String key);
void setByKey(String key, Object value);
}
1
2
3
4
abstract class StringCache {
String getByKey(String key);
void setByKey(String key, String value);
}
1
2
3
4
abstract class Cache<T> {
T getByKey(String key);
void setByKey(String key, T value);
}

1.6.2 限制泛型类型

使用泛型类型的时候, 可以使用 extends 实现参数类型的限制。

1
2
3
4
class Foo<T extends SomeBaseClass> {
// Implementation goes here...
String toString() => "Instance of 'Foo<$T>'";
}

1.7 库和可见性

1.7.1 使用库

import 参数只需要一个指向库的 URI。 对于内置库,URI 拥有自己特殊的dart: 方案。 对于其他的库,使用系统文件路径或者 package: 方案

1.7.2 指定库前缀

如果导入两个存在冲突标识符的库, 则可以为这两个库,或者其中一个指定前缀。 例如,如果 library1 和 library2 都有一个 Element 类, 那么可以通过下面的方式处理:

1
2
3
4
5
6
7
8
import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;

// 使用 lib1 中的 Element。
Element element1 = Element();

// 使用 lib2 中的 Element。
lib2.Element element2 = lib2.Element();

1.7.3 导入库的一部分

如果你只使用库的一部分功能,则可以选择需要导入的 内容。例如:

1
2
3
4
5
// Import only foo.
import 'package:lib1/lib1.dart' show foo;

// Import all names EXCEPT foo.
import 'package:lib2/lib2.dart' hide foo;

延迟加载库

Deferred loading (也称之为 lazy loading) 可以让应用在需要的时候再加载库。 下面是一些使用延迟加载库的场景:

  • 减少 APP 的启动时间。
  • 执行 A/B 测试,例如 尝试各种算法的 不同实现。
  • 加载很少使用的功能,例如可选的屏幕和对话框。

要延迟加载一个库,需要先使用 deferred as 来导入:

1
import 'package:greetings/hello.dart' deferred as hello;

当需要使用的时候,使用库标识符调用 loadLibrary() 函数来加载库:

1
2
3
4
Future greet() async {
await hello.loadLibrary();
hello.printGreeting();
}

在前面的代码,使用 await 关键字暂停代码执行一直到库加载完成。 关于 asyncawait 的更多信息请参考 异步支持

在一个库上你可以多次调用 loadLibrary() 函数。但是该库只是载入一次。

使用延迟加载库的时候,请注意一下问题:

  • 延迟加载库的常量在导入的时候是不可用的。 只有当库加载完毕的时候,库中常量才可以使用。
  • 在导入文件的时候无法使用延迟库中的类型。 如果你需要使用类型,则考虑把接口类型移动到另外一个库中, 让两个库都分别导入这个接口库。
  • Dart 隐含的把 loadLibrary() 函数导入到使用 deferred as *的命名空间* 中。 loadLibrary() 方法返回一个 Future

1.8 异步支持

1.8.1 Future

  • await关键字必须在async函数内部使用
  • 调用async函数必须使用await关键字
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import 'dart:async';

Future<void> printDailyNewsDigest() async {
var newsDigest = await gatherNewsReports();
print(newsDigest);
}


printWinningLotteryNumbers() {
print('Winning lotto numbers: [23, 63, 87, 26, 2]');
}

printWeatherForecast() {
print("Tomorrow's forecast: 70F, sunny.");
}

printBaseballScore() {
print('Baseball score: Red Sox 10, Yankees 0');
}

const news = '<gathered news goes here>';
const oneSecond = Duration(seconds: 1);

// 模拟服务器demo
Future<String> gatherNewsReports() =>
Future.delayed(oneSecond, () => news);

main() {
printDailyNewsDigest();
printWinningLotteryNumbers();
printWeatherForecast();
printBaseballScore();
}

// 执行结果
[min@TATEMIN-MB0] demo $ dart future_demo.dart
Winning lotto numbers: [23, 63, 87, 26, 2]
Tomorrow's forecast: 70F, sunny.
Baseball score: Red Sox 10, Yankees 0
<gathered news goes here>